#!/usr/bin/env python
"""Script server based on SimpleHTTPServer

Handles GET and POST requests, in-memory session management,
HTTP redirection

Python scripts are executed in a namespace made of :
- request : for the data received from the query string or the request body. 
            Calling 'http://host/myScript.py?foo=bar' will make 
            request = {'foo':['bar']} available in the namespace of myScript
- headers : the http request headers
- resp_headers : the http response headers
- Session() : a function returning the session object
- HTTP_REDIRECTION : an exception to raise if the script wants to redirect
to a specified URL (raise HTTP_REDIRECTION, url)

A simple templating system is provided, using the Python string substitution
mechanism introduced in Python 2.4 (syntax $name). Template files must have
the extension .tpl

Hello world programs : will print "Hello world !" if called with the query
string ?name=world
- hello.py (Python script) [ http://localhost/hello.py?name=world ]
   print "Hello",request['name'][0],"!"
- hello.tpl (template)  [ http://localhost/hello.tpl?name=world ]
   Hello $name !

Other extensions can be handled by adding methods self.run_(extension)
"""

import sys
import os
import string
import cStringIO
import random
import cgi
import select
import SimpleHTTPServer
import Cookie

chars = string.ascii_letters + string.digits
sessionDict = {} # dictionary mapping session id's to session objects

class SessionElement(object):
   """Arbitrary objects, referenced by the session id"""
   pass

def generateRandom(length):
	"""Return a random string of specified length (used for session id's)"""
	return ''.join([random.choice(chars) for i in range(length)])

class HTTP_REDIRECTION(Exception):
	pass

class ScriptRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
	"""One instance of this class is created for each HTTP request"""

	def do_GET(self):
		"""Begin serving a GET request"""
		# build self.body from the query string
		self.body = {}
		if self.path.find('?')>-1:
			qs = self.path.split('?',1)[1]
			self.body = cgi.parse_qs(qs, keep_blank_values=1)
		self.handle_data()
		
	def do_POST(self):
		"""Begin serving a POST request. The request data is readable
		on a file-like object called self.rfile"""
		ctype, pdict = cgi.parse_header(self.headers.getheader('content-type'))
		length = int(self.headers.getheader('content-length'))
		if ctype == 'multipart/form-data':
			self.body = cgi.parse_multipart(self.rfile, pdict)
		elif ctype == 'application/x-www-form-urlencoded':
			qs = self.rfile.read(length)
			self.body = cgi.parse_qs(qs, keep_blank_values=1)
		else:
			self.body = {}				   # Unknown content-type
		# some browsers send 2 more bytes...
		[ready_to_read,x,y] = select.select([self.connection],[],[],0)
		if ready_to_read:
			self.rfile.read(2)
		self.handle_data()

	def handle_data(self):
		"""Process the data received"""
		self.resp_headers = {"Content-type":'text/html'} # default
		self.cookie=Cookie.SimpleCookie()
		if self.headers.has_key('cookie'):
			self.cookie=Cookie.SimpleCookie(self.headers.getheader("cookie"))
		path = self.get_file() # return a file name or None
		if os.path.isdir(path):
			# list directory
			dir_list = self.list_directory(path)
			self.copyfile(dir_list, self.wfile)
			return
		ext = os.path.splitext(path)[1].lower()
		if len(ext)>1 and hasattr(self,"run_%s" %ext[1:]):
			# if run_some_extension() exists
			exec ("self.run_%s(path)" %ext[1:])
		else:
			# other files
			ctype = self.guess_type(path)
			if ctype.startswith('text/'):
				mode = 'r'
			else:
				mode = 'rb'
			try:
				f = open(path,mode)
				self.resp_headers['Content-type'] = ctype
				self.resp_headers['Content-length'] = str(os.fstat(f.fileno())[6])
				self.done(200,f)
			except IOError:
				self.send_error(404, "File not found")

	def done(self, code, infile):
		"""Send response, cookies, response headers 
		and the data read from infile"""
		self.send_response(code)
		for morsel in self.cookie.values():
			self.send_header('Set-Cookie', morsel.output(header='').lstrip())
		for (k,v) in self.resp_headers.items():
			self.send_header(k,v)
		self.end_headers()
		infile.seek(0)
		self.copyfile(infile, self.wfile)

	def get_file(self):
		"""Set the Content-type header and return the file open
		for reading, or None"""
		path = self.path
		if path.find('?')>1:
			# remove query string, otherwise the file will not be found
			path = path.split('?',1)[0]
		path = self.translate_path(path)
		if os.path.isdir(path):
			for index in "index.html", "index.htm":
				index = os.path.join(path, index)
				if os.path.exists(index):
					path = index
					break
		return path

	def run_py(self, script):
		"""Run a Python script"""
		# redirect standard output so that the "print" statements 
		# in the script will be sent to the web browser
		sys.stdout = cStringIO.StringIO()

		# build the namespace in which the script will be run
		namespace = {'request':self.body, 'headers' : self.headers,
			'resp_headers':self.resp_headers, 'Session':self.Session,
			'HTTP_REDIRECTION':HTTP_REDIRECTION}
		try:
			execfile (script,namespace)
		except HTTP_REDIRECTION,url:
			self.resp_headers['Location'] = url
			self.done(301,cStringIO.StringIO())
		except:
			# print a traceback
			self.print_traceback(script)
		self.resp_headers['Content-length'] = sys.stdout.tell()
		self.done(200,sys.stdout)

	def print_traceback(self, script):
		"""
		This code does not show everything I need so I am skipping it.
		Paul
		"""
		import traceback
		# first reset the output stream
		sys.stdout = cStringIO.StringIO()
		traceback.print_exc(file=sys.stdout)

	def run_tpl(self,script):
		"""Templating system with the string substitution syntax
		introduced in Python 2.4"""

		# values must be strings, not lists
		dic = dict([ (k,v[0]) for k,v in self.body.items() ])
		# first check if the string.Template class is available
		if hasattr(string,"Template"): # Python 2.4 or above
			try:
				data = string.Template(open(script).read()).substitute(dic)
			except:
				exc_type,exc_value,tb=sys.exc_info()
				msg = exc_value.args[0]
				data = '%s in file %s : %s' \
					%(exc_type.__name__,os.path.basename(script), 
					cgi.escape(msg))
		else:
			data = "Unable to handle this syntax for " + \
				"string substitution. Python version must be 2.4 or above"
		self.resp_headers['Content-length'] = len(data)
		self.done(200,cStringIO.StringIO(data))

	def Session(self):
		"""Session management
		If the client has sent a cookie named sessionId, take its value and 
		return the corresponding SessionElement objet, stored in 
		sessionDict
		Otherwise create a new SessionElement objet and generate a random
		8-letters value sent back to the client as the value for a cookie
		called sessionId"""
		if self.cookie.has_key("sessionId"):
			sessionId=self.cookie["sessionId"].value
		else:
			sessionId=generateRandom(8)
			self.cookie["sessionId"]=sessionId
		try:
			sessionObject = sessionDict[sessionId]
		except KeyError:
			sessionObject = SessionElement()
			sessionDict[sessionId] = sessionObject
		return sessionObject


class writer:
	"""
	Class for unbuffered file writing
	"""
	def __init__(self, logfile):
		self.logfo = open(logfile, "a")

	def __del__(self):
		self.close()

	def write(self, str):
		self.logfo.write(str)
		self.logfo.flush()

	def close(self):
		if not self.logfo.close():
			self.logfo.close()
	

class daemon:
	def write_pid(self, name):
		"""
		Writes a pid file for name in /var/run if not already there.
		It will stop a process and overwrite an existing file.

		>>> s = daemon()
		>>> vname = "testing123"
		>>> s.write_pid(vname)
		1
		>>> s.write_pid(vname)
		1
		>>> os.remove("./%s.pid" % vname)
		"""
		if os.name == "java": # does not work in jython
			return 0		  # return value of 0 is failure or False

		pidfilename = "./%s.pid" % name
		if os.path.exists(pidfilename):
			pid = open(pidfilename).read()
			try:
				os.kill(int(pid), 15)
			except OSError:
				pass
		fo = open(pidfilename, "w")
		fo.write(str(os.getpid()))
		fo.close()
		return 1
	
	def daemonize(self, process_name=""):
		"""Disk And Execution MONitor (Daemon)
	
		Default daemon behaviors (they can be modified):
		1.) Ignore SIGHUP signals.
		2.) Default current working directory to the "/" directory.
		3.) Set the current file creation mode mask to 0.
		4.) Close all open files (0 to [SC_OPEN_MAX or 256]).
		5.) Redirect standard I/O streams to "/dev/null".
	
		Failed fork() calls will return a tuple: (errno, strerror).  This behavior
		can be modified to meet your program's needs.
	
		Resources:
			Advanced Programming in the Unix Environment: W. Richard Stevens
			Unix Network Programming (Volume 1): W. Richard Stevens
			http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
	
		__author__ = "Chad J. Schroeder"
		__version__ = "$Id$"
	
		Detach this process from the controlling terminal and run it in the
		background as a daemon.
		"""
		if os.name == "java": # does not work in jython
			return 0		  # return value of 0 is failure or False

		import signal
	
		try:
			# Fork a child process so the parent can exit.  This will return control
			# to the command line or shell.  This is required so that the new process
			# is guaranteed not to be a process group leader.  We have this guarantee
			# because the process GID of the parent is inherited by the child, but
			# the child gets a new PID, making it impossible for its PID to equal its
			# PGID.
			pid = os.fork()
		except OSError, e:
			return((e.errno, e.strerror))	  # ERROR (return a tuple)
	
		if (pid == 0):		 # The first child.
	
			# Next we call os.setsid() to become the session leader of this new
			# session.  The process also becomes the process group leader of the
			# new process group.  Since a controlling terminal is associated with a
			# session, and this new session has not yet acquired a controlling
			# terminal our process now has no controlling terminal.  This shouldn't
			# fail, since we're guaranteed that the child is not a process group
			# leader.
			os.setsid()
	
			# When the first child terminates, all processes in the second child
			# are sent a SIGHUP, so it's ignored.
			signal.signal(signal.SIGHUP, signal.SIG_IGN)
	
			try:
				# Fork a second child to prevent zombies.  Since the first child is
				# a session leader without a controlling terminal, it's possible for
				# it to acquire one by opening a terminal in the future.  This second
				# fork guarantees that the child is no longer a session leader, thus
				# preventing the daemon from ever acquiring a controlling terminal.
				pid = os.fork()		  # Fork a second child.
			except OSError, e:
				return((e.errno, e.strerror))  # ERROR (return a tuple)
	
			if (pid == 0):		# The second child.
				# Ensure that the daemon doesn't keep any directory in use.  Failure
				# to do this could make a filesystem unmountable.
				#os.chdir("/")
				# Give the child complete control over permissions.
				os.umask(0)
			else:
				os._exit(0)		# Exit parent (the first child) of the second child.
		else:
			os._exit(0)			# Exit parent of the first child.
	
		# Close all open files.  Try the system configuration variable, SC_OPEN_MAX,
		# for the maximum number of open files to close.  If it doesn't exist, use
		# the default value (configurable).
		try:
			maxfd = os.sysconf("SC_OPEN_MAX")
		except (AttributeError, ValueError):
			maxfd = 256		 # default maximum
	
		for fd in range(0, maxfd):
			try:
				os.close(fd)
			except OSError:	# ERROR (ignore)
				pass
	
		# Redirect the standard file descriptors to /dev/null.
		os.open("/dev/null", os.O_RDONLY)	 # standard input (0)
		os.open("/dev/null", os.O_RDWR)		 # standard output (1)
		os.open("/dev/null", os.O_RDWR)		 # standard error (2)

		if process_name:
			self.write_pid(process_name)
		return 1 # If we got here we succeeded return True

class runner:
	def _serve(self):
		# launch the server on the specified port
		import SocketServer
		dm = daemon()
		dm.daemonize(sys.argv[0])
		sys.stdout = writer("%s.log" % sys.argv[0])
		sys.stderr = sys.stdout
		port = 9080
		s=SocketServer.TCPServer(('',port),ScriptRequestHandler)
		print "ScriptServer running on port %s" %port
		s.serve_forever()

	def _test(self):
		pass

	def _doctest(self):
		import doctest, app_server
		doctest.testmod(app_server)

if __name__ == "__main__":
	r = runner()
	getattr(r, "_"+sys.argv[1])()

